package bot;

import extmodule.ExtProposalMC;
import extmodule.IF_ExtModule_Feed;
import extmodule.JNI_GetProposal;
import game.Action;
import game.Player;
import game.Round;
import game.State;
import game.gamestorage.IF_SetGameStorage;
import game.gamestorage.texas.db.SetGameStorageTH_DB;
import game.model.Card;
import glassfrog.model.Gamedef;

import java.io.FileNotFoundException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;

import util.DebugOut;
import util.GameTypeInt;
import util.Helper;

import bot.config.BotConfig;
import bot.module.IF_GetEquity;
import bot.module.IF_GetEstimation;
import bot.module.IF_GetProposal;
import bot.module.equity.SubjectiveAllinEquity;
import bot.module.th.estimation.Estimation;
import bot.module.th.shc.*;
import bot.talk.Talk;
import bot.talk.Talk_ACPC;
import static util.poker.texas.BetInt.*;

/**
 * The frame for the whole bot. Strategy is to be implemented in a child class.
 * @author Witthold/Korol
 */
public abstract class Bot implements IF_Bot_Talk {

	SecureRandom random;

	protected String myname;
	protected Integer buyin;

	protected State state;

	protected Talk talk;

	/*
	 * Modules are held in Lists by Interfaces as module group for the ability of having more than one module of
	 * same module group
	 */
	protected List<IF_GetProposal> proposals = new ArrayList<IF_GetProposal>();
	protected List<IF_GetEquity> equities = new ArrayList<IF_GetEquity>();
	protected List<IF_GetEstimation> estimations = new ArrayList<IF_GetEstimation>();

	protected List<IF_ExtModule_Feed> extFeeds = new ArrayList<IF_ExtModule_Feed>();

	protected List<IF_SetGameStorage> gameStorage = new ArrayList<IF_SetGameStorage>();


	/*
	 * abstract methods
	 */

	/**
	 * The specific bot can decide, if he want to / can play this kind of game.
	 */
	protected abstract boolean isPlayable();


	/**
	 * Returns the actual bet, which is to be sent to the server.</br> The first
	 * integer represents the kind of the bet as defined in BetInt.java</br> The
	 * second integer is only necessary when the amount is to be specified.
	 * 
	 * @see util.poker.texas.BetInt
	 * @return The calculated bet
	 */
	public abstract int[] getBet();


	/*
	 * concrete methods
	 */

	/**
	 * Reads in the bots configuration file and initializes the bot accordingly.
	 * 
	 * @param config
	 *            - bots configuration file
	 */
	public Bot(String config) {

		random = new SecureRandom();

		try {
			List<HashMap<String, String>> botconfig = BotConfig.getBotConfig(config);

			for (HashMap<String, String> section : botconfig) {
				if (section.get("Section").equals("GENERAL")) {

					DebugOut.showVerboseBotConfig("Section: general");

					String botName = section.get("botname");
					if (botName != null) {
						DebugOut.showVerboseBotConfig("bot name: " + botName);
						myname = botName;

					} else {

						myname = "default_botname";
					}

					try {

						buyin = Integer.valueOf(section.get("buyin"));

					} catch (NumberFormatException e) {

						buyin = 1000;
					}

				} else if (section.get("Section").equals("SERVER")) {

					if (section.get("protocol").equals("ACPC")) {
						if (section.get("version").equals("1.0")) {

							int room = Integer.parseInt(section.get("port"));
							String serverAdress = section.get("IP");

							talk = new Talk_ACPC(this, serverAdress, room);
						}
					}

				} else if (section.get("Section").equals("GAMESTORAGE")) {

					gameStorage.add(new SetGameStorageTH_DB());

					if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null && !((SetGameStorageTH_DB) gameStorage.get(0)).connectionSuccess()) {
						System.err.println("ERROR: No connection to db. Shutting down... ");
						System.exit(0);
					}

				} else if (section.get("Section").equals("STARTINGHANDCHART")) {

					if (section.get("name").equals("BSS")) {
						DebugOut.showVerboseBotConfig("proposals.add(new TableBSS())");
						proposals.add(new TableBSS());
					} else {
						proposals.add(new TableCSV(section.get("name")));
					}

				} else if (section.get("Section").equals("POTODDS")) {

					equities.add(new SubjectiveAllinEquity(GameTypeInt.GAME_HOLDEM, 5)); // TODO read these arguments of the gamedef

				} else if (section.get("Section").equals("BLUFFING")) {

				} else if (section.get("Section").equals("SQUEEZING")) {

				} else if (section.get("Section").equals("OPPONENTRECOGNITION")) {

					DebugOut.showVerboseBotConfig("opponent recognition:");
					if (section.get("name").equals("estimation")) {
						DebugOut.showVerboseBotConfig("Loading estimation");
						
						// if forgotten to activate the gamestorage, do it now
						if (gameStorage.isEmpty() || !(gameStorage.get(0) instanceof SetGameStorageTH_DB)){
							gameStorage.add(new SetGameStorageTH_DB());
						}

						if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null && !((SetGameStorageTH_DB) gameStorage.get(0)).connectionSuccess()) {
							System.err.println("ERROR: No connection to db. Shutting down... ");
							System.exit(0);
						}

						/* process begin_at - estimate competitions newer than */
						String begin_at = section.get("begin_at");
						DebugOut.showVerboseBotConfig(begin_at);
						Calendar begin_atCal = Calendar.getInstance();
						long beginAt;

						if (begin_at.equals("0")) { /* allTime */
							beginAt = 0l;
						} else if (begin_at.equals("now")) { /* only upcoming competition */
							beginAt = begin_atCal.getTimeInMillis();
						} else { /* custom time in format: YYYY-MM-DD-hh-mm-ss */
							String[] begin_atSAr = Helper.myStringTokenizer(begin_at, "-");
							int[] begin_atIAr = new int[begin_atSAr.length];
							for (int i = 0; i < begin_atSAr.length; i++) {
								begin_atIAr[i] = Integer.parseInt(begin_atSAr[i]);
							}
							/* months start at 0 ;) - yes, it's true */
							begin_atIAr[1] -= 1;
							begin_atCal.set(begin_atIAr[0], begin_atIAr[1], begin_atIAr[2], begin_atIAr[3], begin_atIAr[4], begin_atIAr[5]);
							beginAt = begin_atCal.getTimeInMillis();
						}

						/*
						 * process restrict_to_players - restrict Estimation on
						 * explicit players - perhaps for reasons of time
						 */
						String restrict_to_players = section.get("restrict_to_players");
						DebugOut.showVerboseBotConfig(restrict_to_players);
						ArrayList<String> restrictToPlayers = new ArrayList<String>();

						if (!restrict_to_players.equals("0")) { /* restriction wanted */
							String[] restrict_to_playersSAr = Helper.myStringTokenizer(restrict_to_players, ",");

							for (int i = 0; i < restrict_to_playersSAr.length; i++) {
								restrictToPlayers.add(restrict_to_playersSAr[i]);
							}
						}

						estimations.add(new Estimation(getName(), beginAt, restrictToPlayers));
					}

				} else if (section.get("Section").equals("EXT_PROPOSAL")) {

					if (section.get("name").equals("mc")) {

						DebugOut.showVerboseBotConfig("Loading MC proposal");
						extFeeds.add(new ExtProposalMC());

					} else {

						DebugOut.showVerboseBotConfig("Loading  ExtProposal");
						extFeeds.add(new JNI_GetProposal());
					}

				} else {
					System.err.println("NO modules loaded");
				}
			}

		} catch (FileNotFoundException e1) {
			e1.printStackTrace();
			// TODO Init default config via java?
		}
	}


	/**
	 * Calls the run method of the talk object if the talk object is
	 * initialized.
	 */
	public void run() {
		if (talk != null && !talk.initConnection()) {
			System.exit(0);
		}

		/* Keep connection to server */
		talk.run();
	}


	/**
	 * Returns a random bet (10% folding; 70% calling; 30% default raise)
	 * 
	 * @return int[0]: -1, 1 or 2
	 */
	protected int[] getRandomBet() {

		int[] action = new int[2];

		int random = Helper.getRandom(10);

		switch (random) {
		case 0:
			action[0] = FOLD;
			break;
		case 1:
		case 2:
		case 3:
			action[0] = RAISE;
			break;
		default:
			action[0] = CALL;
			break;
		}

		return action;
	}


	/**
	 * If it is the bot's turn, this method calls the getBet() method, converts
	 * the move and sends it to the talk object.
	 */
	public void handleStateChange() {

		int numActives = state.getActivePlayers().length;
		if (state.isMyMove() && numActives > 1) { // if only one active player is left, the hand is over

			int[] proposal = getBet();

			Player self = state.getPlayers()[0];
			int[] cashs = new int[state.getActivePlayers().length];

			for (int i = 0; i < cashs.length; i++) {
				Player player = state.getActivePlayers()[i];
				cashs[i] = player.getCurrentCash() + player.getCurrentBet();
			}

			/* process proposal */
			switch (proposal[0]) {
			case FOLD:
				talk.sendFold();
				break;

			case CHECK:
				if (state.isCheckable()) {
					talk.sendCall();
				} else {
					talk.sendFold();
				}
				break;

			case CALL:
				talk.sendCall();
				break;

			case CALL20:
				if (isCall20able(cashs, state.getHighestBet() - self.getCurrentBet())) {
					talk.sendCall();
				} else {
					talk.sendFold();
				}
				break;

			case MINRAISE:
				talk.sendRaise();
				break;

			case RAISE3:

			case RAISE4:

			case RAISE:
				talk.sendRaise(self.getCurrentBet() + proposal[1]);
				break;

			case ALLIN:
				talk.sendRaise(self.getCurrentBet() + self.getCurrentCash());
				break;

			default:
				System.err.println("default switch case: sending call");
				talk.sendCall();
				break;
			}
		} else {
			// TODO can be something done, if it isn't our turn? (estimation, ...)
		}
	}


	/**
	 * Initializes a new game.</br> Generates a new state with the given
	 * gamedef, searches the database for matching gamedef-ID (only if
	 * estimation module is loaded) and calls isPlayable().
	 * 
	 * @see isPlayable()
	 * @param gamedef
	 *            The definition of the game
	 * @return The result of isPlayable()
	 */
	public boolean newGame(Gamedef gamedef) {

		// TODO something else here?
		state = new State(gamedef);

		if (!estimations.isEmpty() && ((Estimation) estimations.get(0)) != null) {
			/*
			 * get database gamedef_id matching this gamedef, analyse completed
			 * competitions right now
			 */
			int gamedefId = setGetGamedef();
			((Estimation) estimations.get(0)).estimateCompletedCompetitions(gamedefId); // TODO fill via
			// botconfig
		}

		return isPlayable();
	}


	/**
	 * Writes the the current state to the console.
	 */
	public void printState() {

		state.printState();
	}


	/**
	 * Allocates the profits of the players by name.</br> 
	 * <b>Needs the showdown(int[]) method.</b>
	 * 
	 * @param names
	 *            Names of the winners
	 * @param gains
	 *            Gains of the corresponding winners
	 */
	public void showdown(String[] names, int[] gains) {

		int[] winners = new int[names.length];

		for (int i = 0; i < winners.length; i++) {

			winners[i] = state.getPlayerByName(names[i]);
		}

		int[] profitsPerPlayer = new int[state.getPlayers().length];

		for (int i = 0; i < winners.length; i++) {

			profitsPerPlayer[winners[i]] = gains[i];

		}

		showdown(profitsPerPlayer);
	}


	/**
	 * Allocates the profits of the players by the players order (the own index
	 * is at int[0]).
	 * 
	 * @param profitsPerPlayer
	 *            Profits of the players
	 */
	public void showdown(int[] profitsPerPlayer) {

		state.showdown(profitsPerPlayer);

		provideShowdownInformation(profitsPerPlayer);

		endHand();
	}


	/**
	 * Stores names & buyins of the players.
	 * 
	 * @param names
	 *            all names of the players
	 * @param buyins
	 *            buy ins of the players
	 */
	public void setPlayers(String[] names, int[] buyins) {

		state.setPlayers(names, buyins);
	}


	/**
	 * Sets the blinds accordingly to the game definition.
	 */
	public void setBlinds() {

		int[][] blinds = state.setGetBlinds();

		for (int i = 0; i < blinds.length; i++) {

			Action action = new Action(0, blinds[i][0], 'b', blinds[i][1]);
			provideNewActionInformation(action);
		}
	}


	/**
	 * Sets the hand counter to the given and updates the position of the
	 * players.</br> If it is the first hand, it also arranges the players so
	 * that the own index is 0.
	 * 
	 * @param ownPosition
	 * 			  The own position in this hand
	 * @param hand
	 *            The number hand of the current hand.
	 */
	public void setHand(int position, int hand) {

		state.setHand(position, hand);

		if (hand == 0) {

			if (!extFeeds.isEmpty()) {
				initExtProposal();
			}

			storeCompetitionStartData();

			if (!estimations.isEmpty() && ((Estimation) estimations.get(0)) != null) {
				if (!((Estimation) estimations.get(0)).initNewCompetition()) {
					System.err.println("Error: Bot.setHand: !estimation.initNewCompetition()");
				}
			}
		}

		provideNewHandInformation(state.getButton(), 0);
	}


	/**
	 * Sets the hole cards of the given player.
	 * 
	 * @param player
	 * 			  Player who has the given hole cards
	 * @param cards
	 * 			  Given board cards
	 */
	public void setHoleCards(int player, Card[] cards) {

		state.setHoleCards(player, cards);

		provideNewHoleCardsInformation(player, cards);
	}


	public void endLastRound(Card[][] cards) {

		state.setHoleCards(cards);

		state.setCurrentPlayer(-1);
		// for (int i = 0; i < cards.length; i++) {
		//			
		// if( cards[i] != null ) {
		// provideNewHoleCardsInformation(i, cards[i]);
		// }
		// }
	}


	/**
	 * Sets the end of the hand by updating the state.</br>
	 * Tries to store the complete hand data into database.
	 */
	public void endHand() {

		state.setCurrentPlayer(-1);

		storeHandStartData();

		int currentRound = state.getCurrentRound();

		for (int i = 0; i <= currentRound; i++) {

			storeRoundEndData(i);
		}

		storeHandEndData();

		if (!estimations.isEmpty() && ((Estimation) estimations.get(0)) != null) {
			if (!((Estimation) estimations.get(0)).estimate()) {
				System.err.println("Error: Bot.endHand: !estimation.estimate()");
			}
		}
	}


	/**
	 * Updates
	 */
	public void setBoardCards(int round, Card[] cards) {

		state.setBoardCards(round, cards);

		if (cards != null) {

			provideNewRoundInformation(round, cards);
		}
	}


	public void setNextActivePlayer() {

		state.setNextActivePlayer();
	}


	public void addAction(int round, char actionChar) {

		Action action = state.addAction(round, actionChar);

		provideNewActionInformation(action);
	}


	public void addAction(int round, char actionChar, int finalInPot) {

		Action action = state.addAction(round, actionChar, finalInPot);

		provideNewActionInformation(action);
	}


	public void newRound() {

		state.newRound();
	}


	public String getName() {
		return myname;
	}


	public int getBuyin() {
		return buyin;
	}


	/**
	 * @return gamedef_id
	 */
	public int setGetGamedef() {

		if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null) {

			/* game dependent */
			int limited = state.getLimit();
			int minBet = state.getMinBet();
			int maxBet = state.getMaxBet();
			int numPlayers = state.getGamedefsMinPlayers();
			int style = state.getStyle();
			int buyin = state.getBuyin();

			/* send data */
			return gameStorage.get(0).setGetGamedef(limited, minBet, maxBet, numPlayers, style, buyin);
		}
		return -1;
	}


	public void storeCompetitionStartData() {

		if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null) {

			/* player dependent */
			Player[] players = state.getPlayers();
			String[] playerNames = new String[players.length];
			int[] initCash = new int[players.length];

			for (int i = 0; i < players.length; i++) {

				playerNames[i] = players[i].getName();
				initCash[i] = players[i].getCurrentCash();
			}

			/* send data */
			gameStorage.get(0).setStartCompetition(playerNames, initCash);
		}
	}


	public void storeHandStartData() {

		if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null) {

			/* game dependent */
			int sb = state.getSmallBlind();
			int bb = state.getBigBlind();
			int ante = state.getAnte();

			/* player dependent */
			Player[] players = state.getPlayers();

			int[] positions = new int[players.length];
			int[] cashInits = new int[players.length];

			Card[] holeCards = players[0].getHoleCards();

			for (int i = 0; i < players.length; i++) {

				positions[i] = players[i].getCurrentPosition();
				cashInits[i] = players[i].getCashAtStartOfHand();
			}

			/* send data */
			((SetGameStorageTH_DB) gameStorage.get(0)).setHand(positions, sb, bb, ante, cashInits, holeCards);
		}
	}


	public void storeRoundEndData() {

		if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null) {

			storeRoundEndData(state.getCurrentRound());
		}
	}


	public void storeRoundEndData(int n) {

		if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null) {

			Round round = state.getRound(n);

			long timestamp = round.getTimestamp();
			ArrayList<Action> actions = round.getHistory();
			Card[] boardCards = round.getBoardCards();

			/* send data */
			((SetGameStorageTH_DB) gameStorage.get(0)).setEndRound(timestamp, actions, boardCards);
		}
	}


	public void storeHandEndData() {

		if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null) {

			int finalpot = state.getPotSize();
			int[] cashDeltas = state.getCashDeltas();

			Player[] players = state.getPlayers();
			Card[][] holeCards = new Card[players.length][];

			for (int i = 0; i < players.length; i++) {

				holeCards[i] = players[i].getHoleCards();
			}

			/* send data */
			((SetGameStorageTH_DB) gameStorage.get(0)).setEndHand(finalpot, cashDeltas, holeCards);
		}
	}


	public void storeCompetitionEndData() {

		if (!gameStorage.isEmpty() && (gameStorage.get(0)) != null) {

			int[] rankings = state.getRankings();

			/* send data */
			gameStorage.get(0).setEndCompetition(rankings);
		}
	}


	public void gameOver() {

		storeCompetitionEndData();
	}


	public int getNumPlayers() {

		return state.getPlayers().length;

	}


	private boolean isCall20able(int[] cashs, int raiseDelta) { // TODO move
		// this method
		// into OurBot?

		// TODO verify by Mick
		/** at least one other active player has that cash */
		if (cashs[0] >= 20 * raiseDelta) {
			for (int i = 1; i < cashs.length; i++) {
				if (cashs[i] >= 20 * raiseDelta) {
					return true;
				}
			}
		}
		return false;
	}


	private void initExtProposal() {

		int[] betSizes = new int[2];
		betSizes[0] = state.getMinBet();
		betSizes[1] = state.getMaxBet();

		int[] betStructure = state.getBetStructure();
		int[] blindStructure = state.getBlindStructure();

		int ante = state.getAnte();

		Player[] players = state.getPlayers();

		StringBuffer namesBuffer = new StringBuffer();

		int[] cashs = new int[players.length];

		for (int i = 0; i < players.length; i++) {

			namesBuffer.append(players[i].getName() + " ");
			cashs[i] = players[i].getCurrentCash();
		}

		for (int i = 0; i < extFeeds.size();) {

			boolean loaded = extFeeds.get(i).initModule(betSizes, betStructure, blindStructure, ante, namesBuffer.toString(), cashs);

			if (loaded) {

				i++;

			} else {

				extFeeds.remove(i);

			}
		}

		if (extFeeds.isEmpty()) {
		}
	}


	private void provideNewHandInformation(int button, int round) {

		if (!extFeeds.isEmpty()) {
			for (IF_ExtModule_Feed proposal : extFeeds) {

				// proposal.feedInformation(button, round, -1, null, null, null,
				// -1);
				proposal.newHand(button);
			}
		}
	}


	// private void provideBlindInformation(int player, Action action) {
	//	
	// if( extProposals != null ) {
	// for (ExtProposal proposal : extProposals) {
	//				
	// proposal.feedInformation(-1, -1, player, action, null, null, -1);
	// }
	// }
	// }

	private void provideNewHoleCardsInformation(int player, Card[] cards) {

		if (!extFeeds.isEmpty()) {
			for (IF_ExtModule_Feed proposal : extFeeds) {

				// proposal.feedInformation(-1, -1, player, null, cards, null,
				// -1);
				int[] intCards = new int[cards.length];
				for (int i = 0; i < cards.length; i++) {

					intCards[i] = cards[i].toInt();
				}
				proposal.newHoleCards(player, intCards);
			}
		}
	}


	private void provideNewActionInformation(Action action) {

		if (!extFeeds.isEmpty()) {
			for (IF_ExtModule_Feed proposal : extFeeds) {

				// proposal.feedInformation(-1, -1, action.getPlayer(), action,
				// null, null, -1);
				proposal.newAction(action);
			}
		}
	}


	private void provideNewRoundInformation(int round, Card[] newCards) {

		if (!extFeeds.isEmpty()) {
			for (IF_ExtModule_Feed proposal : extFeeds) {

				// proposal.feedInformation(-1, round, -1, null, null, newCards,
				// -1);
				int[] intCards = new int[newCards.length];
				for (int i = 0; i < newCards.length; i++) {

					intCards[i] = newCards[i].toInt();
				}

				proposal.newRound(round, intCards);
			}
		}
	}


	private void provideShowdownInformation(int[] winsPerPlayer) {

		if (!extFeeds.isEmpty()) {
			for (IF_ExtModule_Feed proposal : extFeeds) {

				for (int i = 1; i < state.getPlayers().length; i++) {

					Card[] cards = state.getPlayers()[i].getHoleCards();

					if (cards != null) {
						provideNewHoleCardsInformation(i, cards);
					}
				}

				for (int i = 0; i < winsPerPlayer.length; i++) {

					// proposal.feedInformation(-1, -1, i, null, null, null,
					// winsPerPlayer[i]);
					proposal.showdown(i, winsPerPlayer[i]);
				}
			}
		}
	}
}
